package de.fhkl.whatsstroke;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.Toast;
import de.fhkl.whatsstroke.StrokeService.StrokeBinder;

public class Record extends Activity
{
	/**
	 * The file extension to use to play the video file. Must match the file
	 * extension used in WhatsStroke.
	 */
	private static final String FILE_EXTENSION = ".mp4";

	private Camera mCamera;
	private CameraPreview mPreview;
	private MediaRecorder mMediaRecorder;

	private boolean mIsRecording;

	private ImageButton mStartButton;
	private ImageButton mNextButton;
	private ImageView mInstructionImage;

	private UUID mVideoId;

	private enum Step
	{
		EYE_TEST, NOSE_TEST, SPEECH_TEST, WALK_TEST, END_TEST;
	}

	private Step mCurrentStep;

	private CountDownTimer mTimer;

	private boolean mClickable;

	/**
	 * ServiceConnection to retrieve a reference for a save way to call methods
	 * from the service
	 */
	private ServiceConnection mConnection;

	/** Reference to the underlying service */
	private StrokeService mService;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);

		// Must be called before creating content!

		// as long as this window is visible to the user, keep the device's
		// screen turned on and bright.
		this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

		// hide title bar
		this.requestWindowFeature(Window.FEATURE_NO_TITLE);

		// displays this activity in fullscreen mode.
		this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

		// Now start creating content.

		setContentView(R.layout.activity_record);

		setResult(RESULT_CANCELED);

		// No camera on this device
		if (!checkCameraHardware(this))
		{
			Log.e("Record", "No camera found on this device!");
			finish();
		}

		mTimer = new ButtonDelayTimer(1000, 1000);

		mVideoId = UUID.randomUUID();

		createContent();

		// In most cases this won't do anything. Service should be already
		// running. Let's try to start it just in case it doesn't.
		Intent intent = new Intent(getApplicationContext(), StrokeService.class);
		getApplicationContext().startService(intent);

		mConnection = new StrokeServiceConnection();
	}

	private void createContent()
	{
		mStartButton = (ImageButton) findViewById(R.id.start_button);
		mStartButton.setVisibility(View.VISIBLE);
		mStartButton.setOnClickListener(new StartButtonListener());

		mNextButton = (ImageButton) findViewById(R.id.next_button);
		mNextButton.setVisibility(View.INVISIBLE);
		mNextButton.setOnClickListener(new NextButtonListener());

		mInstructionImage = (ImageView) findViewById(R.id.instruction_text);
		mInstructionImage.setVisibility(View.INVISIBLE);

		mCurrentStep = Step.EYE_TEST;

		mCamera = getCameraInstance();

		// Create our Preview view and set it as the content of our activity.
		mPreview = new CameraPreview(this, mCamera);
		FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
		preview.addView(mPreview);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu)
	{
		return false;
	}

	@Override
	protected void onStart()
	{
		super.onStart();

		this.bindService(new Intent(this, StrokeService.class), mConnection, 0);
	}

	@Override
	protected void onResume()
	{
		super.onResume();

		if (mMediaRecorder == null)
		{
			mMediaRecorder = new MediaRecorder();
		}

		if (mCamera == null)
		{
			mCamera = getCameraInstance();
		}

		try
		{
			mCamera.startPreview();
		}
		catch (Exception e)
		{
		}
	}

	@Override
	protected void onPause()
	{
		releaseMediaRecorder();
		releaseCamera();

		super.onPause();
	}

	@Override
	protected void onStop()
	{
		this.unbindService(mConnection);

		super.onStop();
	}

	@Override
	protected void onDestroy()
	{
		releaseMediaRecorder();
		releaseCamera();

		super.onDestroy();
	}

	/**
	 * Check if this device has a camera.
	 * 
	 * @param context
	 * @return
	 */
	private boolean checkCameraHardware(Context context)
	{
		if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))
		{
			// this device has a camera
			return true;
		}
		else
		{
			// no camera on this device
			return false;
		}
	}

	/**
	 * A safe way to get an instance of the Camera object.
	 * 
	 * @return returns null if camera is unavailable
	 */
	private Camera getCameraInstance()
	{
		Camera c = null;
		try
		{
			// attempt to get a Camera instance
			c = Camera.open();
		}
		catch (Exception e)
		{
			c = null;
			Log.e("Record", "Camera.open() failed! Could not retrieve camera object");

			Toast.makeText(this, R.string.error_camera, Toast.LENGTH_LONG).show();

			finish();
		}

		return c;
	}

	/**
	 * Prepares the video recorder by setting audio and video sources, quality
	 * preferences, file output path etc.
	 * 
	 * @return
	 */
	private boolean prepareVideoRecorder()
	{
		mCamera.stopPreview();
		mMediaRecorder = new MediaRecorder();

		// Step 1: Unlock and set camera to MediaRecorder
		mCamera.unlock();
		mMediaRecorder.setCamera(mCamera);

		// Step 2: Set sources
		mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
		mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

		// Step 3: Set a CamcorderProfile (requires API Level 8 or higher)
		CamcorderProfile cp = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);

		mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
		mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);
		mMediaRecorder.setVideoEncodingBitRate(cp.videoBitRate);
		mMediaRecorder.setVideoFrameRate(cp.videoFrameRate);
		mMediaRecorder.setVideoSize(320, 240);
		mMediaRecorder.setAudioEncoder(cp.audioCodec);
		mMediaRecorder.setAudioEncodingBitRate(cp.audioBitRate);
		mMediaRecorder.setAudioSamplingRate(cp.audioSampleRate);
		mMediaRecorder.setAudioChannels(cp.audioChannels);

		// Step 4: Set output file
		mMediaRecorder.setOutputFile(getOutputMediaFile().toString());

		// Step 5: Set the preview output
		mMediaRecorder.setPreviewDisplay(mPreview.getHolder().getSurface());

		// Step 6: Prepare configured MediaRecorder
		try
		{
			mMediaRecorder.prepare();
		}
		catch (IllegalStateException e)
		{
			Log.e("Record", "IllegalStateException preparing MediaRecorder: " + e.getMessage());
			releaseMediaRecorder();
			return false;
		}
		catch (IOException e)
		{
			Log.e("Record", "IOException preparing MediaRecorder: " + e.getMessage());
			releaseMediaRecorder();
			return false;
		}
		return true;
	}

	/**
	 * Release the media recorder.
	 */
	private void releaseMediaRecorder()
	{
		if (mMediaRecorder != null)
		{
			mMediaRecorder.reset(); // clear recorder configuration
			mMediaRecorder.release(); // release the recorder object
			mMediaRecorder = null;
			mCamera.lock(); // lock camera for later use
		}
	}

	/**
	 * Release the camera for other applications.
	 */
	private void releaseCamera()
	{
		if (mCamera != null)
		{
			try
			{
				mCamera.stopPreview();
			}
			catch (Exception e)
			{
				// trying to stop a non existing preview
			}
			mCamera.release(); // release the camera for other applications
			mCamera = null;
		}
	}

	/**
	 * States of the on screen messages of the single test steps
	 */
	private void changeText()
	{
		mNextButton.setEnabled(false);
		mTimer.start();

		switch (mCurrentStep)
		{
			case EYE_TEST:
				mInstructionImage.setImageResource(R.drawable.eye_test);
				mCurrentStep = Step.NOSE_TEST;
			break;

			case NOSE_TEST:
				mInstructionImage.setImageResource(R.drawable.nose_test);
				mCurrentStep = Step.SPEECH_TEST;
			break;

			case SPEECH_TEST:
				mInstructionImage.setImageResource(R.drawable.speech_test);
				mCurrentStep = Step.WALK_TEST;
			break;

			case WALK_TEST:
				mInstructionImage.setImageResource(R.drawable.walk_test);
				mNextButton.setImageResource(R.drawable.send);
				mCurrentStep = Step.END_TEST;
			break;

			case END_TEST:
				testEnd();
			break;
		}
	}

	/**
	 * Video recording has finished, clean up and forwarding to next activity
	 */
	private void testEnd()
	{
		if (mIsRecording)
		{
			if (mMediaRecorder != null)
			{
				// stop recording and release camera
				mMediaRecorder.stop();
				releaseMediaRecorder();
			}
			if (mCamera != null)
			{
				// take camera access back from MediaRecorder
				mCamera.lock();
				releaseCamera();
			}

			mIsRecording = false;
		}

		AsyncTask<Void, Void, Void> at = new AsyncTask<Void, Void, Void>()
		{
			@Override
			protected Void doInBackground(Void... params)
			{
				mService.videoRecordingStops(mVideoId);

				return null;
			}
		};
		at.execute();

		Intent data = new Intent(this, Outbox.class);
		data.putExtra("de.fhkl.whatsstroke.VideoPath", getOutputMediaFile().getAbsolutePath());
		data.putExtra("de.fhkl.whatsstroke.VideoId", mVideoId);
		setResult(RESULT_OK, data);
		finish();
	}

	/**
	 * Create a file for saving a video
	 */
	private File getOutputMediaFile()
	{
		File mediaFile = null;

		// Check that the SDCard is mounted
		if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
		{
			File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), getText(
					R.string.app_name).toString());

			// Create the storage directory if it does not exist
			if (!mediaStorageDir.exists())
			{
				if (!mediaStorageDir.mkdirs())
				{
					Log.e("WhatsStroke", "failed to create directory");
					return null;
				}
			}

			mediaFile = new File(mediaStorageDir.getPath() + File.separator + mVideoId + FILE_EXTENSION);
		}

		return mediaFile;
	}

	/**
	 * Listener for the StartButton
	 */
	private class StartButtonListener implements View.OnClickListener
	{
		@Override
		public void onClick(View v)
		{
			if (!mIsRecording)
			{
				// initialize video camera
				if (prepareVideoRecorder())
				{
					AsyncTask<Void, Void, Void> at = new AsyncTask<Void, Void, Void>()
					{
						@Override
						protected Void doInBackground(Void... params)
						{
							mService.videoRecordingStarts(mVideoId);
							return null;
						}
					};

					at.execute();

					// Camera is available and unlocked, MediaRecorder is
					// prepared,
					// now you can start recording
					mMediaRecorder.start();

					// inform the user that recording has started
					mStartButton.setVisibility(View.INVISIBLE);
					mNextButton.setVisibility(View.VISIBLE);
					mInstructionImage.setVisibility(View.VISIBLE);

					changeText();
					mClickable = true;

					mIsRecording = true;
				}
				else
				{
					// prepare didn't work, release the camera
					releaseCamera();
					Toast.makeText(Record.this, R.string.error_camera, Toast.LENGTH_LONG).show();
				}
			}
		}
	}

	/**
	 * Listener for the NextButton
	 */
	private class NextButtonListener implements View.OnClickListener
	{
		@Override
		public void onClick(View v)
		{
			if (mClickable)
			{
				mClickable = false;
				changeText();
			}
		}
	}

	/**
	 * Timer for the delay after clicking a button to avoid skipping steps of
	 * the test
	 */
	private class ButtonDelayTimer extends CountDownTimer
	{

		public ButtonDelayTimer(long millisInFuture, long countDownInterval)
		{
			super(millisInFuture, countDownInterval);
		}

		@Override
		public void onFinish()
		{
			mNextButton.post(new Runnable()
			{
				public void run()
				{
					mNextButton.setEnabled(true);
				}
			});

			mClickable = true;
		}

		@Override
		public void onTick(long millisUntilFinished)
		{

		}
	}

	/**
	 * Safe connection to the underlying service over a returned weak reference.
	 * Like many callbacks from the system, the methods on this class are called
	 * from the main thread of your process.
	 */
	public class StrokeServiceConnection implements ServiceConnection
	{

		@Override
		public void onServiceConnected(ComponentName name, IBinder binder)
		{
			mService = ((StrokeBinder) binder).getService();
		}

		@Override
		public void onServiceDisconnected(ComponentName name)
		{
		}
	}
}
